Skip to content

fix(generator): idempotent install + a consumer re-sync guide - #39

Merged
mhenrixon merged 1 commit into
mainfrom
issue-24-idempotent-install-resync
Jul 3, 2026
Merged

fix(generator): idempotent install + a consumer re-sync guide#39
mhenrixon merged 1 commit into
mainfrom
issue-24-idempotent-install-resync

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

What & why

docs_kit:install was unsafe to re-run: create_initializer clobbered a site's edited config, and add_routes duplicated a route when the site had written it in a different style. Consequence: sites never re-ran it and fossilized on the generator output of the day they were created (the audits found both consumer sites still hand-duplicating render_page and carrying a dead IconHelper).

This makes rails g docs_kit:install fully idempotent — safe on a fresh app AND a years-old site — so re-running it becomes the sanctioned upgrade path as this epic keeps adding routes / initializer lines / AGENTS.md across issues.

Closes #24.

Changes

Area Before After
create_initializer re-rendered the template every run (clobbered edited config) skips when present, prints a diff hint at the gem template path
add_routes Thor's byte-exact skip only — duplicated a route the site drew in its own style route_once guard matches controller#action (or root) tolerant of single/double quotes, to: vs =>, whitespace
register_stimulus_controller matched only the eager form — double-registered when a site lazy-loaded skips when wired via either loader (eager or lazy)
--sync flag runs only additive/wiring steps, scaffolds no site content, prints a drift checklist
Drift detection new SyncReport helper: flags a hand-written render_page + a dead IconHelperwarns, never deletes

Design notes

  • A flag on the one generator (not a separate docs_kit:update) keeps the guard logic in one place.
  • Drift detection stays string-level and conservative — it warns, never auto-edits or deletes site code.
  • --sync never re-scaffolds site-owned content (Doc registry, pages, the themed application.tailwind.css); those are the site's.
  • SyncReport extracted to its own file (lib/generators/docs_kit/install/sync_report.rb) per the "many small files" rule.

Test coverage (TDD — RED first)

  • Route idempotency vs a hand-written, differently-styled routes.rb (single quotes, to:, no .:format): no duplicate root / docs#show / docs_kit/search#index; the site's own syntax is left untouched.
  • create_initializer preserves edited config byte-for-byte on re-run + reports the skip/hint.
  • --sync scaffolds no site content (registry/pages/CSS) and is idempotent (a second sync duplicates no routes).
  • --sync drift detection flags render_page + IconHelper and does not delete them.
  • Stimulus: no double-registration when the site already lazy-loads the docs_kit path.

Verification gates

  • bundle exec rspec505 examples, 0 failures, 96.57% line coverage (floor is 80%).
  • bundle exec rubocop — clean (103 files).
  • ✅ Fresh-app path unchanged — the manifest/route/stimulus specs all still pass (on a fresh app nothing pre-exists, so every step runs as before); docs-kit new still runs the full docs_kit:install.

Live --sync runs (as the issue requires)

1. Against the gem's own docs/ app — a no-op with zero drift warnings ✅

skip  config/initializers/docs_kit.rb exists — compare with <gem>/templates/docs_kit.rb.erb if upgrading
skip  site content (--sync: registry/pages are yours)
identical  route docs#show (already drawn)
identical  route docs_kit/search#index (already drawn)
identical  route root (already drawn)
identical  route docs_kit/llms#index (already drawn)
identical  route docs_kit/llms#full (already drawn)
skip  CSS build (--sync: application.tailwind.css is yours)
identical  app/javascript/controllers/index.js
info  docs-kit synced.

The config initializer diff was empty; a second sync produced zero route changes.

2. Against phlex-reactive/docs — flags render_page + IconHelper

insert  app/controllers/application_controller.rb   (adds `include DocsKit::Controller`)
identical  route docs#show (already drawn)            ← site wrote get 'docs/:doc' => 'docs#show' (single quotes)
identical  route root (already drawn)                 ← site wrote root 'landings#show' (single quotes)
route  get "/docs/search" …                           ← genuinely missing, added
route  get "/llms.txt" … / "/llms-full.txt" …         ← genuinely missing, added
identical  app/javascript/controllers/index.js        ← site lazy-loads docs_kit; NOT double-registered
warn  manual cleanup needed (docs-kit now provides these):
  • app/controllers/application_controller.rb defines its own render_page — delete it; DocsKit::Controller#render_page is included.
  • app/helpers/icon_helper.rb (IconHelper) is dead — docs-kit renders icons via rails_icons (DocsUI::Icon); delete it.

Both drift files were left on disk (warn-only). The consumer's single-quote routes were correctly recognized as already-drawn and not duplicated — the exact bug this issue was about. Its lazy Stimulus registration was preserved, not double-registered (a bug this verification surfaced and this PR also fixes).

Live runs were executed against copies of both apps in a scratchpad; neither real repo was modified.

Out of scope (per the issue)

  • Editing the consumer repos (they run --sync themselves).
  • Version-aware migrations between docs-kit releases (post-1.0).

https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX

Makes `rails g docs_kit:install` safe to re-run on a years-old site, so
re-running it becomes the sanctioned upgrade path (rather than sites
fossilizing on the generator output of the day they were created).

## Summary
- create_initializer skips (never clobbers) a site's edited
  config/initializers/docs_kit.rb; prints a diff hint at the gem template.
- add_routes is now quote/syntax tolerant: a route the site already drew
  (single vs double quotes, `to:` vs `=>`) is skipped, not duplicated.
- register_stimulus_controller skips when the docs_kit path is already
  wired via EITHER loader (eager OR lazy) — no more double-registration.
- New `--sync` flag: runs only the additive/wiring steps (routes,
  initializer hint, importmap/Stimulus, AGENTS.md, .rubocop.yml), never
  re-scaffolds site-owned content, and prints a conservative drift
  checklist (hand-written render_page, dead IconHelper) — warns, never
  auto-deletes. Extracted to a SyncReport helper class.
- README "Keeping a site in sync" section + one-time cleanup table;
  CHANGELOG Added/Fixed entries.

## Test Coverage
- route idempotency against a hand-written, differently-styled routes.rb
  (single quotes, `to:`, no `.:format`) — no duplicate root/docs#show/search
- create_initializer preserves edited config byte-for-byte on re-run
- --sync scaffolds no site content (registry/pages/CSS) and is idempotent
- --sync drift detection flags render_page + IconHelper, never deletes them
- stimulus: no double-register when the site already lazy-loads docs_kit

## Verification
- [x] bundle exec rubocop passes (103 files)
- [x] bundle exec rspec passes (505 examples, 96.57% line coverage)
- [x] Live --sync vs the gem's own docs/ app: no-op, ZERO drift warnings
- [x] Live --sync vs phlex-reactive/docs: flags render_page + IconHelper,
      skips its single-quote routes, no double stimulus registration

Closes #24

Claude-Session: https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX
@mhenrixon mhenrixon self-assigned this Jul 3, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Jul 3, 2026
@mhenrixon
mhenrixon merged commit c23e7d5 into main Jul 3, 2026
4 checks passed
@mhenrixon
mhenrixon deleted the issue-24-idempotent-install-resync branch July 4, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(generator): idempotent install + a consumer re-sync guide

1 participant